#!/usr/bin/python
"""Sequence is used for representing a binding site, probably in an Alignment file.

So basically a bunch of Sequences make an Alignment.
"""

# by Vineet Gupta <vineet@ural.wustl.edu>
# sequence.py 1-27-2007 05:34 

import random

class LengthMismatchException(Exception):
    "Raised when sequences having different length are operated together."
    def __init__(self):
        Exception.__init__(self)
        print "This operation is not allowed on Sequences of different length."

class Sequence:
    "Class for representing simple DNA sequence/binding site."
    def __init__(self, seq=""):
        if seq:
            # If any sequence is given to start, encode it and set it as data
            self.data = self.encode(seq)
        else:
            # Nothing given, intialise with empty data
            self.data = []
            
    def __ReturnLength(self):
        return len(self.decode(self.data))

    # Cool use of new class style for holding total number of bases in Sequence
    Length = property(fget=__ReturnLength, doc="Length of Sequence in bases")
            
    def set(self, seq):
        "Set the bases for sequence. To be used if initialised empty or changed later."
        self.data = self.encode(seq)

    def print_seq(self):
        "Print all the bases in given sequence. Don't use this method; use print Sequence directly."
        seq = self.decode(self.data)
        print seq

    def encode(self, seq):
        "Encode data in 3 bit form, from ACGT representation and return it."
        data = []
        # Doing all work in lower case
        seq = seq.lower()
        
        for base in seq:
            if base == 'a':
                data.append('1');data.append('-1');data.append('1')
            elif base == 'c':
                data.append('-1');data.append('-1');data.append('-1')
            elif base == 'g':
                data.append('-1');data.append('1');data.append('1')
            elif base == 't':
                data.append('1');data.append('1');data.append('-1')
            else:
                data.append('0');data.append('0');data.append('0')

        return data

    def decode(self, data):
        "Decode data from 3 bit form to ACGT form and return it."
        seq = ""

        for i in xrange(0, len(data), 3):
            s = data[i] + data[i+1] + data[i+2]

            if s == "1-11":
                seq += 'a'
            elif s == "-1-1-1":
                seq += 'c'
            elif s == "-111":
                seq += 'g'
            elif s == "11-1":
                seq += 't'
            else:
                seq += 'n'

        return seq
    
    def distance(self, otherSeq):
        "Returns distance between main Sequence and argument Sequence."
        dist = 0
        
        if self.Length != otherSeq.Length:
            raise LengthMismatchException
        else:
            for i in xrange(3 * self.Length):
                dist += abs((int)(self.data[i]) - (int)(otherSeq.data[i]))
                
        return dist
    
    def minDistance(self, otherSeq):
        '''Returns the overestimate for distance between otherSeq and given seq.
        
        It makes rest of bases in otherSeq(which is of lesser length than given seq)
        same as given seq and then calculates the normal distance between them.
        '''
        len1 = len(otherSeq.data)
        len2 = len(self.data)
        for i in xrange(len1, len2):
            (otherSeq.data).append(self.data[i])
            
        return self.distance(otherSeq)
    
    def dotProduct(self, otherSeq):
        "Returns dot product between main Sequence and argument Sequence."
        product = 0
        
        if self.Length != otherSeq.Length:
            raise LengthMismatchException
        else:
            for i in xrange(3 * self.Length):
                product += (int)(self.data[i]) * (int)(otherSeq.data[i])
                
        return product
    
    def __repr__(self):
        "Print representation for Sequence class."
        seq = self.decode(self.data)
        return seq
    
    def print_raw(self):
        "Print Sequence in form of encoded representation."
        for i in self.data:
            print i,
            
        print
    
class RandomSequence(Sequence):
    "A class similar to Sequence but it intialises sequence randomly given length."
    def __init__(self, length):
        bases = ['a', 'c', 'g', 't']
        seq = ""
        
        for tmp in xrange(length):
            seq += random.choice(bases)
            
        Sequence.__init__(self, seq)
        
class Consensus(Sequence):
    "A class for representing Consensus sequence. It could be intialised to \
    anything but it hardly makes any sense for consensus."
    def __init__(self, seq=""):
        "Same as parent class."
        Sequence.__init__(self, seq)
        
    def randomize(self,  length):
        "Set Consensus to some random sequence having given length."
        options = ['-1', '0','1']
        # Clear all previous items
        self.data = []
        
        for tmp in xrange(3*length):
            self.data.append(random.choice(options))
            